import java.io.*;
import java.net.*;
import java.util.*;
import java.rmi.*;
import java.rmi.registry.*;
import java.lang.reflect.*;

public class client {

	private Hashtable<String, Boolean> queryq;

	private boolean running;

	private String configPath;

	private rmibroker controller;

	private rmibroker serviceLookup;

	private final String BINDING = "cshields";
	
	private final String RMIPORT = "1337";

	public static void main(String[] args) {

		if (args.length < 2 || args.length > 3) {

			System.out.println("USAGE: java client <hostname> <port> [configFilePath]");

			return;
		}

		String file = "";

		if (args.length == 3) {
			file = args[2];
		}

		client c = new client(args[0], args[1], file);

		c.start();

	}

	// this is the central method for the client.
	public client(String ip, String port, String configPath) {

		this.queryq = new Hashtable<String, Boolean>();

		this.configPath = configPath;

		// Insert opening and closing instructions for the connection

		try {

			System.out.print("Creating object to perform remote invocation...");

			Registry registry = LocateRegistry.getRegistry(ip, Integer.parseInt(port));
			this.serviceLookup = (rmibroker) registry.lookup(BINDING);

			System.out.println("done");

		} catch (Exception e) {
			System.out.println(e.getMessage());
			return;
		}

	}

	private void start() {
		try {
			this.running = true;

			int MESSAGE = 0;

			// Get message queue
			MsgFile mfile = new MsgFile(configPath);

			// send data
			while (running) {

				// get next message
				String m = mfile.next();
				String[] fields = null;

				if (m != null) {
					m = format(m);

					// split the command
					fields = m.split(" ");

					if (!checkFieldValidity(fields)) {
						continue;
					}
				}

				// This if statement handles each case for possible client
				// messages
				// for each case the message to the server is formatted and an
				// id is added if necessary

				// Check for end of commands, shutdown server if so
				if (m == null || fields[MESSAGE].equalsIgnoreCase(msg.End)) {

					System.out.println("Ending client session");

					this.running = false;

					// If message is query, then add id and send it out
				} else {

					int ACTION = 0;

					String id = getAlphaNumID();

					// format the query properly, unless its malformed, in which
					// case leave the latter part blank and let the server
					// complain
					String returnValue = this.serviceLookup.query(id, fields[ACTION], "*");
					if(returnValue != null && !returnValue.equals("") && !returnValue.equalsIgnoreCase("FALSE"))	
						process(returnValue, m, msg.Response);
					else
						System.out.println("No such service");
				}

			}

		} catch (Exception e) {
			System.out.println("Service broker offline");
		}

	}

	// format the input string

	private String format(String s) {

		StringBuilder formatted = new StringBuilder();

		boolean space = false;

		for (int cnt = 0; cnt < s.length(); cnt++) {

			if (s.charAt(cnt) == ' ' && !space) {
				space = true;
			} else if (s.charAt(cnt) == '\n') {
				continue;
			} else if (s.charAt(cnt) == ' ' && space) {
				continue;
			} else {
				space = false;
			}

			formatted.append(s.charAt(cnt));

		}

		return formatted.toString();
	}

	// check to ensure that the input is valid

	private boolean checkFieldValidity(String[] fs) {

		for (String f : fs) {

			if (f.length() > 64) {
				System.out.println("Field lengths must be < 64 characters");
				return false;
			}

			try {

				long value = Long.parseLong(f);

				if (value > 2000000000) {
					System.out.println("Numeric value: " + f
							+ " must be < 2 billion");
					return false;
				}

			} catch (NumberFormatException e) {
			}
		}
		return true;

	}

	// generate a unique id for the given message
	private String getAlphaNumID() {

		StringBuilder sb = null;

		// 48 and 57
		// 97 and 122
		int ALPHA_MIN = 97;
		int ALPHA_MAX = 122;
		int NUM_MIN = 48;
		int NUM_MAX = 57;

		Random gen = new Random();

		int flip = 0;

		do {
			sb = new StringBuilder();

			while (sb.length() < 6) {

				int num = -1;

				if (flip == 0) {
					num = gen.nextInt(ALPHA_MAX - ALPHA_MIN) + ALPHA_MIN;
					flip = 1;
				} else {
					flip = 0;
					num = gen.nextInt(NUM_MAX - NUM_MIN) + NUM_MIN;
				}

				sb.append((char) num);

			}

		} while (this.queryq.containsKey(sb.toString()));

		return sb.toString();
	}

	// process the reply from the server
	private void process(String r, String argString, String type) {
		String[] fields = r.split(" ");

		int SERVICE = 0;
		int IP = 1;
		argString = argString.replaceFirst("\\w+?\\s|\\w+$","");
		if (!r.startsWith("ERROR")) {
			try {
				// Perform reflection call here to execute operation
				Class serviceProvider = Class.forName(fields[SERVICE] + "_service");
				
				// Create the needed arrays for invoking the method
				Object[] args = null;
				Class[] types = null;
				boolean date = fields[SERVICE].equalsIgnoreCase("DATE");
				if(!date) {
				//THIS IS FOR NON_DATE instructions						
					args = argString.split(" ");
					types = new Class[args.length];					

					// instantiate the arrays
					for (int cnt = 0; cnt < types.length; cnt++) {
						types[cnt] = String.class;
					}
					
					if(args.length != 2){
						System.out.println(msg.E3 + ": invalid arguments");
						return;
					}
				}//Handle date instruction
				else{
					args = new String[]{argString};
					types = new Class[]{String.class};
				}
				
				Method executor = serviceProvider.getMethod(fields[SERVICE], types);
				String uri = String.format("rmi://%s:%s/%s", fields[IP], RMIPORT, fields[SERVICE]);
				System.out.println("Using service: " + uri);

				Object target = Naming.lookup(uri);
				
				Object returnValue = executor.invoke(target, args);
				if(date) 
					System.out.printf("date %s: %s%n", argString, returnValue.toString());
				else 
					System.out.printf("%s(%s,%s) = %s%n", fields[SERVICE], args[0], args[1], returnValue.toString());

			}catch(java.rmi.ConnectException e){
				System.out.println(fields[SERVICE] + " service provider not available");
			}catch (Exception e) {
				System.out.println(e.getMessage());
			}

		} else if ((r).equalsIgnoreCase(msg.E2)) {
			this.running = false;
		} else
			System.out.println("Server response: " + r);

	}

	// This class represents the input, either a file or std in
	class MsgFile {

		private BufferedReader reader;
		
		private boolean interactive;

		private ArrayList<String> msgQue;

		public MsgFile(String f) {

			this.msgQue = new ArrayList<String>();

			if (!f.equals("")) {
				System.out.println("Processing the script file: " + f);
				this.msgQue = read(f);
				interactive = false;
			} else {
				try {
					reader = new BufferedReader(new InputStreamReader(System.in));
					interactive = true;
				} catch (Exception e) {
					System.out.println(e.getMessage());
					this.msgQue.add(msg.Bye);
				}
			}

		}

		// close the input stream
		public void close() {
			try {
				if (reader != null) {
					reader.close();
				}
			} catch (IOException e) {
				System.out.println(e.getMessage());
			}
		}

		// get the next input string
		public String next() {
			if (msgQue.isEmpty() && !interactive) {
				return msg.End;
			}
			if (!msgQue.isEmpty() && !interactive) {
				return msgQue.remove(0);
			} else if(interactive) {
				try {

					String newline = "";

					boolean redo = false;

					do {

						redo = false;

						newline = reader.readLine();
						if (newline == null)
							return msg.End;

						newline = newline.trim();

						// remove comment symbol
						int index = newline.indexOf("%");

						if (index >= 0) {
							newline = newline.substring(0, index);
						}

						// check for blank line
						if (newline.equals("")) {
							redo = true;
						}

					} while (redo);

					return newline;

				} catch (IOException e) {
					System.out.println(e.getMessage());
					return msg.End;
				}
			}
			return msg.End;
		}

		// read in the stream to get a list of instructions
		private ArrayList<String> read(String f) {

			try {

				BufferedReader r = new BufferedReader(new FileReader(f));

				msgQue = new ArrayList<String>();

				String line = "";

				while ((line = r.readLine()) != null) {

					int index = line.indexOf("%");

					String sub = line;

					if (index >= 0) {
						sub = line.substring(0, index);
					}

					// If the line is blank then just skip it.l
					if (sub.equals("")) {
						continue;
					}

					msgQue.add(sub);

				}

				r.close();
			} catch (IOException e) {
				System.out.println(e.getMessage());
			}

			return msgQue;
		}
	}
}
